package struct_utils

import (
	"fmt"
	"reflect"
)

/*
StructBind values with the same tags will be bound and assigned

tags: structs
special tags:

	 	embed : embed struct
		- : ignore bind values
*/
func StructBind[K, V any](bind K, target *V) error {
	return bindValues(getStructFieldMap(bind), target)
}

func bindValues(bindMap map[string]reflect.Value, target interface{}) error {
	tTarget := reflect.TypeOf(target).Elem()
	vTarget := reflect.ValueOf(target).Elem()

	for i := 0; i < tTarget.NumField(); i++ {
		field := tTarget.Field(i)
		tag := field.Tag.Get("structs")
		// ignore field
		if tag == "-" {
			continue
		}
		// embed struct
		if tag == "embed" && field.Type.Kind() == reflect.Struct {
			embedValue := vTarget.Field(i)
			err := bindValues(bindMap, embedValue.Addr().Interface().(interface{}))
			if err != nil {
				return err
			}
			continue
		}
		if vTarget.CanSet() {
			bindValue, ok := bindMap[tag]
			if ok && vTarget.Field(i).Kind() == bindValue.Kind() {
				vTarget.Field(i).Set(bindValue.Convert(field.Type))
			} else {
				return fmt.Errorf("type mismatch: %s", tag)
			}
		}
	}
	return nil
}

func getStructFieldMap[K any](bind K) map[string]reflect.Value {
	bindMap := make(map[string]reflect.Value)

	tBind := reflect.TypeOf(bind)
	vBind := reflect.ValueOf(bind)

	for i := 0; i < tBind.NumField(); i++ {
		field := tBind.Field(i)
		tag := field.Tag.Get("structs")
		// ignore field
		if tag == "-" {
			continue
		}
		// embed struct
		if tag == "embed" && field.Type.Kind() == reflect.Struct {
			embedFieldMap := getStructFieldMap(vBind.Field(i).Interface())
			for k, value := range embedFieldMap {
				bindMap[k] = value
			}
			continue
		}
		bindMap[tag] = vBind.Field(i)
	}

	return bindMap
}
